﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the GNU General Public License version 2 (GPLv2).
// See accompanying file License.txt or copy at http://phuse.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Phuse.Net.Forums
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.IO;
    using System.Linq;

    using Phuse.IO;

    /// <summary>See <see cref="IInboundArticle"/>.</summary>
    internal partial class InboundArticle : IInboundArticle, IInboundArticleInfo
    {
        private const int FilesPerGroup = 100;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Initializes a new instance of the <see cref="InboundArticle"/> class.</summary>
        public InboundArticle()
        {
        }

        /// <summary>See <see cref="IInboundArticle.IsSelected"/>.</summary>
        public bool IsSelected
        {
            get
            {
                return this.Id == EntityHelper.AssertNotDeleted(this.Group).SelectedInboundArticleId;
            }

            set
            {
                this.AssertValid();
                this.Group.SelectedInboundArticleId = value ? this.Id : (int?)null;
            }
        }

        /// <summary>See <see cref="IInboundArticle.HasFullArticleBeenReceived"/>.</summary>
        public bool HasFullArticleBeenReceived
        {
            get { return this.ArticleOffset != null; }
        }

        /// <summary>See <see cref="IInboundArticle.GetContent"/>.</summary>
        public Stream GetContent()
        {
            var group = EntityHelper.AssertNotDeleted(this.Group);
            var path = GetDataFilePath(group.DataDirectory, group.CachedInboundArticlesLimit, this.Number);
            var directory = Path.GetDirectoryName(path);
            var provider = this.Root.FileIOProvider;

            if (this.HasFullArticleBeenReceived)
            {
                // We allow read-write to prevent disruption of simultaneous body receive
                using (var guard = ScopeGuard.Create(
                    provider.OpenFile(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
                using (var segmentGuard = ScopeGuard.Create(
                    new SegmentStream(guard.Resource, (long)this.ArticleOffset, (long)this.ArticleLength)))
                {
                    guard.Dismiss();
                    segmentGuard.Dismiss();
                    return segmentGuard.Resource;
                }
            }
            else
            {
                return new MemoryStream(new byte[0], false);
            }
        }

        /// <summary>See <see cref="IInboundArticle.GetFollowupInboundArticles"/>.</summary>
        public IEnumerable<IInboundArticle> GetFollowupInboundArticles()
        {
            return this.Root.GetFollowupInboundArticles(this).Cast<IInboundArticle>();
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="IInboundArticle.Group"/>.</summary>
        IGroup IInboundArticle.Group
        {
            get { return EntityHelper.AssertNotDeleted(this.Group); }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal InboundArticle(int? referencedId, int? originalId, bool receiveContent, Group group)
        {
            // We're assigning a unique id here rather than have the database do it for the following reasons:
            // 1. For performance reasons, OriginalId cannot be a proper foreign key, see OriginalId implementation for
            //    the rationale. Because it is not a proper foreign key, we must directly assign OriginalId itself
            //    rather than having an association. Because we want to commit multiple articles at once, which likely
            //    reference each other, the not yet committed articles must already have a proper id.
            // 2. The fact that newly created articles already have a valid id when they are not yet committed to the
            //    database allows for much smaller entries in the messageIdMap variable, which needs to be kept in
            //    memory during the whole receive process.
            this.Id = ++group.Root.MaxInboundArticleId;
            this.ReferencedId = referencedId;
            this.OriginalId = originalId;
            this.ReceiveContent = receiveContent;
            this.groupRef.Entity = group;
        }

        internal Root Root
        {
            get { return EntityHelper.AssertNotDeleted(this.Group).Root; }
        }

        internal void SetArticle(long offset, long length)
        {
            this.ArticleOffset = offset;
            this.ArticleLength = length;
            this.ReceiveContent = false;
        }

        internal static string GetDataFilePath(string groupDataDirectory, int cachedInboundArticlesLimit, long number)
        {
            var articlesPerFile = (cachedInboundArticlesLimit / FilesPerGroup) + 1;
            return Path.Combine(
                groupDataDirectory,
                (number / articlesPerFile * articlesPerFile).ToString(CultureInfo.InvariantCulture) + ".dat");
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private void AssertValid()
        {
            this.Root.AssertValid();
        }
    }
}
